package org.deltaset.samples.mvc.web;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.inject.Inject;
import javax.servlet.http.HttpServletRequest;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
import org.deltaset.faces.PageHelper;
import org.deltaset.faces.component.SelectOption;
import org.deltaset.meta.Finder;
import org.deltaset.meta.CollectionHandler;
import org.deltaset.meta.EntityResolver;
import org.deltaset.meta.ExecutorResult;
import org.deltaset.meta.MetaAction;
import org.deltaset.meta.MetaActionExecutor;
import org.deltaset.meta.MetaActionHandler;
import org.deltaset.meta.MetaEntry;
import org.deltaset.meta.MetaField;
import org.deltaset.meta.OptionBuilder;
import org.deltaset.samples.mvc.meta.MetaCrudService;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;

/**
 *
 * @author dmakariev
 */
@Controller
@RequestMapping("/view/{entityClassName}")
public class ViewController {

    private final MetaCrudService metaCrudService;
    private final ViewState viewState;
    private final MetaActionCache metaActionCache;

    @Inject
    public ViewController(MetaCrudService metaCrudService, ViewState viewState, MetaActionCache metaActionCache) {
        this.metaCrudService = metaCrudService;
        this.viewState = viewState;
        this.metaActionCache = metaActionCache;
    }

    @RequestMapping(method = RequestMethod.GET)
    public String showList(
            @PathVariable("entityClassName") String entityClassName,
            @RequestParam(value = "page", required = false) Integer page,
            @RequestParam(value = "size", required = false) Integer size,
            @RequestParam(value = "finderId", required = false) String finderId,
            Model model,
            HttpServletRequest request) {
        //addDateTimeFormatPatterns(uiModel);
        //uiModel.addAttribute("owner", Owner.findOwner(id));
        //uiModel.addAttribute("itemId", id);

        final MetaEntry metaEntry = this.metaCrudService.getMetaEntry(entityClassName);
        final MetaActionHandler actionHandler = this.metaCrudService.getMetaActionHandler();
        final CollectionHandler collectionHandler = this.metaCrudService.getCollectionHandler();
        final DateFormat dateFormat = getDateFormat();
        final OptionBuilder<SelectOption> optionBuilder = this.metaCrudService.getOptionBuilder();
        final MetaHelper helper = new MetaHelper(optionBuilder, metaActionCache, actionHandler, collectionHandler, dateFormat);

        final String pageNum = (null == page) ? "1" : String.valueOf(page);
        final String pageSize = (null == size) ? "5" : String.valueOf(size);

        final boolean existFinderId = null != finderId && !"".equals(finderId);
        // if(null!=finderId&&!"".equals(finderId)){
        final MetaAction finderMetaAction = (existFinderId) ? this.metaActionCache.getMetaActionById(finderId) : null;
        final MetaActionExecutor finderExecutor = (existFinderId) ? this.metaCrudService.getExecutor(finderMetaAction) : null;
        final boolean finderHasParameter = (existFinderId) ? finderMetaAction.getHasParameter() : false;



        if (finderHasParameter) {
            //fill finderExecutor parameterValue

            final List<MetaField> parameterMetaFields = finderMetaAction.getParameterMetaFields();

            final Object parameterValue = finderExecutor.getParameterValue();

            for (MetaField metaField : parameterMetaFields) {

                final String propertyName = metaField.getPropertyName();
                final String requestPropertyName = "_f_" + propertyName;
                final Class propertyType = metaField.getOriginalType();
                final String propertyValue = request.getParameter(requestPropertyName);
                try {
                    setPropertyValue(parameterValue, propertyName, propertyValue, propertyType);
                } catch (Exception ex) {
                    helper.addError(requestPropertyName, ex.toString() + " " + ex.getMessage());
                }
            }
        }


        final Object finderParameterValue = (finderHasParameter) ? finderExecutor.getParameterValue() : null;

        final MetaAction defaultFinderAction = metaEntry.getDefaultFinder();

        final String currentFinderId = (existFinderId) ? helper.actionId(finderMetaAction) : helper.actionId(defaultFinderAction);

        final Finder finder = actionHandler.newPageHelper(finderExecutor, defaultFinderAction);

        final PageHelper<Object> pageHelper = this.metaCrudService.all(finder, pageNum, pageSize);

        model.addAttribute("entityClassName", entityClassName);
        model.addAttribute("page", pageNum);

        model.addAttribute("metaEntry", metaEntry);

        model.addAttribute("pageInfo", pageHelper.getPageInfo());
        model.addAttribute("listItems", pageHelper.getItems());
        model.addAttribute("helper", helper);

        model.addAttribute("finderHasParameter", finderHasParameter);
        model.addAttribute("finderParameterValue", finderParameterValue);

        model.addAttribute("finderId", currentFinderId);
        model.addAttribute("finderMetaAction", finderMetaAction);


        // finderId
        //uiModel.addAttribute("desktop", desktopBean);
        return "view/list";
    }

    @RequestMapping(value = "/{recordId}", method = RequestMethod.GET)
    public String showDetail(
            @PathVariable("entityClassName") String entityClassName,
            @PathVariable("recordId") String recordId,
            Model model) {
        final MetaEntry metaEntry = this.metaCrudService.getMetaEntry(entityClassName);
        final MetaAction findByIdAction = metaEntry.getFindByIdAction();
        final MetaActionHandler actionHandler = this.metaCrudService.getMetaActionHandler();
        final CollectionHandler collectionHandler = this.metaCrudService.getCollectionHandler();
        final DateFormat dateFormat = getDateFormat();
        final OptionBuilder<SelectOption> optionBuilder = this.metaCrudService.getOptionBuilder();
        final MetaHelper helper = new MetaHelper(optionBuilder, metaActionCache, actionHandler, collectionHandler, dateFormat);
        final Class entityClass = metaEntry.getEntityClass();
        final Object recordValue = actionHandler.loadModel(entityClass, findByIdAction, recordId);
        this.viewState.setRecordValue(recordValue);

        model.addAttribute("recordValue", recordValue);
        model.addAttribute("entityClassName", entityClassName);
        model.addAttribute("metaEntry", metaEntry);
        model.addAttribute("helper", helper);
        return "view/detail";
    }

    @RequestMapping(value = "/{recordId}/form", method = RequestMethod.GET)
    public String showUpdateForm(
            @PathVariable("entityClassName") String entityClassName,
            @PathVariable("recordId") String recordId,
            Model model) {
        final MetaEntry metaEntry = this.metaCrudService.getMetaEntry(entityClassName);
        final MetaAction findByIdAction = metaEntry.getFindByIdAction();
        final MetaActionHandler actionHandler = this.metaCrudService.getMetaActionHandler();
        final CollectionHandler collectionHandler = this.metaCrudService.getCollectionHandler();
        final DateFormat dateFormat = getDateFormat();
        final OptionBuilder<SelectOption> optionBuilder = this.metaCrudService.getOptionBuilder();
        final MetaHelper helper = new MetaHelper(optionBuilder, metaActionCache, actionHandler, collectionHandler, dateFormat);
        final Class entityClass = metaEntry.getEntityClass();
        final Object recordValue = actionHandler.loadModel(entityClass, findByIdAction, recordId);

        this.viewState.setRecordValue(recordValue);

        model.addAttribute("recordValue", recordValue);
        model.addAttribute("entityClassName", entityClassName);
        model.addAttribute("metaEntry", metaEntry);
        model.addAttribute("helper", helper);
        return "view/form";
    }

    @RequestMapping(value = "/form", method = RequestMethod.GET)
    public String showNewForm(
            @PathVariable("entityClassName") String entityClassName,
            Model model) throws InstantiationException, IllegalAccessException {


        final MetaEntry metaEntry = this.metaCrudService.getMetaEntry(entityClassName);
        final Class entityClass = metaEntry.getEntityClass();



        //final MetaAction findByIdAction = metaEntry.getFindByIdAction();
        final MetaActionHandler actionHandler = this.metaCrudService.getMetaActionHandler();
        final CollectionHandler collectionHandler = this.metaCrudService.getCollectionHandler();
        final DateFormat dateFormat = getDateFormat();
        final OptionBuilder<SelectOption> optionBuilder = this.metaCrudService.getOptionBuilder();
        final MetaHelper helper = new MetaHelper(optionBuilder, metaActionCache, actionHandler, collectionHandler, dateFormat);

        //final Object recordValue = entityClass.newInstance();//actionHandler.loadModel(entityClass, findByIdAction, recordId);
        //viewState.setRecordValue(recordValue);
        final Object recordValue = this.viewState.getRecordValue();

        model.addAttribute("recordValue", entityClass.cast(recordValue));
        model.addAttribute("entityClassName", entityClassName);
        model.addAttribute("metaEntry", metaEntry);
        model.addAttribute("helper", helper);
        return "view/form";
    }

    @RequestMapping(value = "/form", method = RequestMethod.POST)
    public String create(
            @PathVariable("entityClassName") String entityClassName,
            @RequestParam(value = "_action", required = true) String actionId,
            Model model,
            HttpServletRequest request) throws InstantiationException, IllegalAccessException, NoSuchMethodException, IllegalArgumentException, InvocationTargetException {

        final MetaEntry metaEntry = this.metaCrudService.getMetaEntry(entityClassName);
        final Class entityClass = metaEntry.getEntityClass();
        final MetaActionHandler actionHandler = this.metaCrudService.getMetaActionHandler();
        final CollectionHandler collectionHandler = this.metaCrudService.getCollectionHandler();
        final DateFormat dateFormat = getDateFormat();
        final OptionBuilder<SelectOption> optionBuilder = this.metaCrudService.getOptionBuilder();
        final MetaHelper helper = new MetaHelper(optionBuilder, metaActionCache, actionHandler, collectionHandler, dateFormat);


        final ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
        final Validator validator = factory.getValidator();


        final List<MetaField> formFields = metaEntry.getFormFields();

        //final Object recordValue = entityClass.newInstance();
        final Object recordValue = viewState.getRecordValue();

        for (MetaField metaField : formFields) {

            final String propertyName = metaField.getPropertyName();
            final String requestPropertyName = "_f_" + propertyName;
            final Class propertyType = metaField.getOriginalType();
            final String propertyValue = request.getParameter(requestPropertyName);
            try {
                setPropertyValue(recordValue, propertyName, propertyValue, propertyType);
            } catch (Exception ex) {
                helper.addError(requestPropertyName, ex.toString() + " " + ex.getMessage());
            }
        }

        if (helper.hasErrors()) {
            model.addAttribute("recordValue", recordValue);
            model.addAttribute("entityClassName", entityClassName);
            model.addAttribute("metaEntry", metaEntry);
            model.addAttribute("helper", helper);
            // addDateTimeFormatPatterns(uiModel);
            return "view/form";
        }

        //System.out.println("recordValue="+recordValue);
        //System.out.println("recordValue.getClass="+recordValue.getClass());



        final Set<ConstraintViolation<Object>> constraintViolations = validator.validate(entityClass.cast(recordValue));
        for (ConstraintViolation<Object> constraintViolation : constraintViolations) {
            try {
                final String propertyName = "_f_" + constraintViolation.getPropertyPath().iterator().next().getName();

                final String propertyMessage = constraintViolation.getMessage();

                helper.addError(propertyName, propertyMessage);

            } catch (Exception ex) {
                //do nothing..
            }
        }

        System.out.println("constraintViolations.isEmpty=" + constraintViolations.isEmpty());





        // Errors errors = new Errors

        if (helper.hasErrors()) {
            model.addAttribute("recordValue", recordValue);
            model.addAttribute("entityClassName", entityClassName);
            model.addAttribute("metaEntry", metaEntry);
            model.addAttribute("helper", helper);
            // addDateTimeFormatPatterns(uiModel);
            return "view/form";
        }


        return executeActionInternal(actionId, entityClassName, model);
    }

    @RequestMapping(value = "/execute", method = RequestMethod.POST)
    public String executeParameterAction(
            @PathVariable("entityClassName") String entityClassName,
            @RequestParam(value = "_action", required = true) String actionId,
            Model model,
            HttpServletRequest request) {
        final MetaAction metaAction = this.metaActionCache.getMetaActionById(actionId);
        final MetaActionExecutor executor = this.metaCrudService.getExecutor(metaAction);

        final MetaActionHandler actionHandler = this.metaCrudService.getMetaActionHandler();
        final CollectionHandler collectionHandler = this.metaCrudService.getCollectionHandler();
        final DateFormat dateFormat = getDateFormat();
        final OptionBuilder<SelectOption> optionBuilder = this.metaCrudService.getOptionBuilder();
        final MetaHelper helper = new MetaHelper(optionBuilder, metaActionCache, actionHandler, collectionHandler, dateFormat);

        final List<MetaField> parameterMetaFields = metaAction.getParameterMetaFields();

        final Object parameterValue = executor.getParameterValue();

        for (MetaField metaField : parameterMetaFields) {

            final String propertyName = metaField.getPropertyName();
            final String requestPropertyName = "_f_" + propertyName;
            final Class propertyType = metaField.getOriginalType();
            final String propertyValue = request.getParameter(requestPropertyName);
            try {
                setPropertyValue(parameterValue, propertyName, propertyValue, propertyType);
            } catch (Exception ex) {
                helper.addError(requestPropertyName, ex.toString() + " " + ex.getMessage());
            }
        }

        if (helper.hasErrors()) {
            model.addAttribute("action", executor.getMetaAction());
            model.addAttribute("actionId", actionId);
            model.addAttribute("parameterValue", parameterValue);
            model.addAttribute("helper", helper);
            model.addAttribute("entityClassName", entityClassName);
            return "view/execute-form";
        }

        //final Object[] parameters = executor.getParameters();
        final Object currentRecordValue = this.viewState.getRecordValue();
        //final ExecutorResult executorResult = actionHandler.executeAction(metaAction, recordValue, parameters);

        final ExecutorResult executorResult = actionHandler.executeAction(executor, currentRecordValue);

        final boolean isEntity = executorResult.isEntity();
        if (true == isEntity) {
            final Object value = executorResult.getValue();
            this.viewState.setRecordValue(value);
            final String resultEntityClass = executorResult.getResultClass().getName();
            // final String recordId = actionHandler.getModelId(value);
            return "redirect:/view/" + resultEntityClass + "/form";
        }


//        model.addAttribute("action", executor.getMetaAction());
//        model.addAttribute("actionId", actionId);
//        model.addAttribute("parameterValue", parameterValue);

        model.addAttribute("executorResult", executorResult);
//        model.addAttribute("helper", helper);
        model.addAttribute("entityClassName", entityClassName);
        return "view/execute-result";


        // throw new UnsupportedOperationException();


    }

    @RequestMapping(params = {"action"}, method = RequestMethod.GET)
    public String executeAction(
            @PathVariable("entityClassName") String entityClassName,
            @RequestParam(value = "action", required = true) String actionId,
            Model model) throws InstantiationException, IllegalAccessException {

        return executeActionInternal(actionId, entityClassName, model);
    }

    private String executeActionInternal(String actionId, String entityClassName, Model model) {
        final MetaAction metaAction = this.metaActionCache.getMetaActionById(actionId);

        final MetaActionHandler actionHandler = this.metaCrudService.getMetaActionHandler();

        final MetaActionExecutor executor = this.metaCrudService.getExecutor(metaAction);

        if (true == metaAction.getHasParameter()) {
            //this.viewState.setExecutor(executor);
            //should show the form for porcessing the action (getting parameters) 

            final CollectionHandler collectionHandler = this.metaCrudService.getCollectionHandler();
            final DateFormat dateFormat = getDateFormat();
            final OptionBuilder<SelectOption> optionBuilder = this.metaCrudService.getOptionBuilder();
            final MetaHelper helper = new MetaHelper(optionBuilder, metaActionCache, actionHandler, collectionHandler, dateFormat);

            final Object parameterValue = executor.getParameterValue();

            model.addAttribute("action", executor.getMetaAction());
            model.addAttribute("actionId", actionId);
            model.addAttribute("parameterValue", parameterValue);
            model.addAttribute("helper", helper);
            model.addAttribute("entityClassName", entityClassName);
            return "view/execute-form";
        } else {
            final Object currentRecordValue = viewState.getRecordValue();
            final ExecutorResult executorResult = actionHandler.executeAction(executor, currentRecordValue);

            //final boolean isContext = executorResult.getExecutedAction().isContext();

            //isReturningVoid something was done to the context. should open the list.

            final boolean isReturningEntity = executorResult.getExecutedAction().getIsReturningEntity();
            //final boolean isFactory = executorResult.getExecutedAction().getIsFactory();
            //isFactory - returns the entity
            if (isReturningEntity) {
                final Object recordValue = executorResult.getValue();
                this.viewState.setRecordValue(recordValue);
                //final String recordId = helper.getModelId(recordValue);
                return "redirect:/view/" + entityClassName + "/form";
            }

            final boolean isReturningVoid = executorResult.getExecutedAction().getIsReturningVoid();
            if (isReturningVoid) {
                return "redirect:/view/" + entityClassName + "/";
            }

            ////
            model.addAttribute("executorResult", executorResult);
            //        model.addAttribute("helper", helper);
            model.addAttribute("entityClassName", entityClassName);
            return "view/execute-result";

        }
        //throw new UnsupportedOperationException("not yet finished executeActionInternal ");
        //shows the list
        //return "redirect:/view/" + entityClassName + "/";
    }

    private void setPropertyValue(Object entityValue, String propertyName, String propertyStringValue, Class propertyType) throws NoSuchMethodException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {

        if (Collection.class.isAssignableFrom(propertyType)) {
            return;
        }


        final Object propertyConvertedValue = convert(propertyType, propertyStringValue);
        if (Map.class.isAssignableFrom(entityValue.getClass())) {
            ((Map) entityValue).put(propertyName, propertyConvertedValue);
        } else {

            final Class entityClass = entityValue.getClass();
            final String setterName = "set" + propertyName.substring(0, 1).toUpperCase() + propertyName.substring(1);

            final Method propertySet = entityClass.getMethod(setterName, propertyType);

            propertySet.invoke(entityValue, propertyConvertedValue);
        }
    }

    private Object convert(Class propertyType, String propertyValue) {

        if (this.metaCrudService.isEntity(propertyType)) {
            return this.metaCrudService.findById(propertyType, propertyValue);
        }

        final Converter converter = getConverterFor(propertyType);
        final Object value = converter.convert(propertyType, propertyValue);
        return value;

    }

    private Converter getConverterFor(Class propertyType) {
        final DateFormat dateFormat = getDateFormat();

        return new Converter() {

            @Override
            public Object convert(Class propertyType, String value) {
                if (null == value) {
                    return value;
                }
                if ("".equals(value)) {
                    return null;
                }
                if (String.class.equals(propertyType)) {
                    return value;
                } else if (Integer.class.equals(propertyType)) {
                    return Integer.valueOf(value);
                } else if (int.class.equals(propertyType)) {
                    return Integer.valueOf(value).intValue();
                } else if (double.class.equals(propertyType)) {
                    return Double.valueOf(value).doubleValue();
                } else if (Double.class.equals(propertyType)) {
                    return Double.valueOf(value);
                } else if (propertyType.isEnum()) {
                    return Enum.valueOf((Class<Enum>) propertyType, value);
                } else if (boolean.class.equals(propertyType)) {
                    return Boolean.valueOf(value).booleanValue();
                } else if (Boolean.class.equals(propertyType)) {
                    return Boolean.valueOf(value);
                } else if (Date.class.equals(propertyType)) {
//                    if ("".equals(value)) {
//                        return null;
//                    }
                    // final DateFormat dateFormat = DateFormat.getDateInstance();
                    // final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");

                    try {
                        return dateFormat.parse(value);
                    } catch (ParseException ex) {
                        Logger.getLogger(ViewController.class.getName()).log(Level.SEVERE, null, ex);
                        throw new RuntimeException("could not parse date " + value, ex);
                    }
                }
                throw new RuntimeException("not found converter for " + propertyType);
            }
        };
    }

    private interface Converter<T> {

        T convert(Class propertyType, String value);
    }

    private DateFormat getDateFormat() {
        final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
        return dateFormat;
    }
}
